//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//    NotificationRulesViewer.xaml.cs
//
// Abstract:
//    
//    WPF control to display the automated notifications list and provide options
//    for processing notifications.
//
//--

using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.OutlookAddIn
{
    /// <summary>
    /// Interaction logic for NotificationRulesViewer.xaml
    /// </summary>
    public partial class NotificationRulesViewer : UserControl
    {
        /// <summary>
        /// Command for "Add" button events.
        /// </summary>
        public readonly static RoutedCommand AddCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Edit" button events.
        /// </summary>
        public readonly static RoutedCommand EditCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Delete" button events.
        /// </summary>
        public readonly static RoutedCommand DeleteCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Up" button events.
        /// </summary>
        public readonly static RoutedCommand UpCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Down" button events.
        /// </summary>
        public readonly static RoutedCommand DownCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Enable rule" checkbox events.
        /// </summary>
        public readonly static RoutedCommand EnableRuleCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Enable all rules" checkbox events.
        /// </summary>
        public readonly static RoutedCommand EnableAllRulesCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Ok" button events.
        /// </summary>
        public readonly static RoutedCommand OkCommand = new RoutedCommand();

        /// <summary>
        /// Command for "Cancel" button events.
        /// </summary>
        public readonly static RoutedCommand CancelCommand = new RoutedCommand();
 
        /// <summary>
        /// Command for "Apply" button events.
        /// </summary>
        public readonly static RoutedCommand ApplyCommand = new RoutedCommand();

        private bool _rulesListIsEmpty;
        private IRulesLoader _rulesLoader;
        
        /// <summary>
        /// Constructor.
        /// </summary>
        public NotificationRulesViewer()
            : this(new RulesLoader())
        {           
        }

        /// <summary>
        /// Constructor for unit tests.
        /// </summary>
        /// <param name="rulesLoader">The rules loader to provide the rules.</param>
        public NotificationRulesViewer(IRulesLoader rulesLoader)
        {
            Debug.Assert(rulesLoader != null, "rulesLoader");

            InitializeComponent(); // Called by WPF to initialize the control.

            _rulesLoader = rulesLoader;

            // Override the default background color so it matches that of the buttons.
            Debug.Assert(stackPanel != null, "stackPanel - has the XAML changed?");
            Debug.Assert(addButton != null, "addButton - has the XAML changed?");
                       
            stackPanel.Background = addButton.Background;
         
            Loaded += OnRulesViewerLoaded;
        }

        /// <summary>
        /// Called when the rules viewer window has been created and ready for user interaction.
        /// </summary>
        private void OnRulesViewerLoaded(object sender, RoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
            Debug.Assert(_rulesLoader != null, "_rulesLoader");

            // Once the component is initialized we should now have a reference to the
            // rules object bound to the ListView.  Take a reference to this.
            NotificationRules rules = Rules;

            try
            {
                _rulesLoader.Load(rules); // Populates the rules list.
            }
            catch (InvalidOperationException 
#if DEBUG
                exn
#endif
                ) // Thrown if an error occurs loading rules from storage.
            {
                string message = Properties.Resources.RulesDialogLoadWarningMessage;
#if DEBUG
                message += " Exception details: " + exn.ToString();
#endif
                // Warn the user if an error occurs while loading the rules and then close the rules
                // dialog window as the rules list is possibly corrupted.
                MessageBox.Show(message, 
                                Properties.Resources.RulesDialogTitle, 
                                MessageBoxButton.OK, 
                                MessageBoxImage.Warning);
                Close();
                return;
            }

            if (rules.Count > 0)
            {
                rulesListView.SelectedIndex = 0;
            }
            else
            {
                HandleEmptyList();
            }

            GenerateRuleDescription();
        }

        /// <summary>
        /// Return the rules collection which is bound to the ListView object.
        /// </summary>
        private NotificationRules Rules
        {
            get
            {
                Debug.Assert(rulesListView != null, "rulesListView");
                Debug.Assert(rulesListView.ItemsSource != null, "rulesListView.ItemsSource");

                return Helpers.AssertCast<NotificationRules>(rulesListView.ItemsSource);
            }
        }

        /// <summary>
        /// Event handler for ListView selection changing.  This is used to prevent the user
        /// from selecting the "List is empty" message.
        /// </summary>
        private void rulesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            if (_rulesListIsEmpty || 
                rulesListView.SelectedIndex < 0) // Deleting the selected rule causes selectedIndex to go to -1.
            {
                rulesListView.SelectedIndex = -1;
                return;
            }

            GenerateRuleDescription();
        }

        /// <summary>
        /// Event handler for "Add" button being clicked.
        /// </summary>
        private void AddRule(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            NotificationRuleItem addedRule = ShowAddEditDialog(/*null rule for adding*/null, /*showActionPage*/false);

            if (addedRule == null)
            {
                return; // Dialog was cancelled.
            }

            NotificationRules rules = Rules; // Cache the Rules instance.

            if (_rulesListIsEmpty) // Remove the "List is empty message".
            {
                _rulesListIsEmpty = false;
                rules.RemoveAt(0);
            }

            rules.Add(addedRule);
            rulesListView.SelectedIndex = rules.Count - 1;

            GenerateRuleDescription();
        }

        /// <summary>
        /// Called by WPF to determine whether to enable the "Add" button or not.
        /// </summary>
        private void AddCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = true;
        }
        
        /// <summary>
        /// Command invoked for editing a rule.
        /// </summary>
        private void EditRule(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
            Debug.Assert(rulesListView != null, "rulesListView");

            EditRule(rulesListView.SelectedIndex);
        }
        /// <summary>
        /// Edit the selected rule.
        /// </summary>
        /// <param name="selectedIndex">Index of the rule to edit.</param>
        private void EditRule(int selectedIndex)
        {
            EditRule(selectedIndex, /*showActionPage*/false);
        }

        /// <summary>
        /// Edit the selected rule.
        /// </summary>
        /// <param name="selectedIndex">Index of the rule to edit.</param>
        /// <param name="showActionPage">True if the rule "actions" view rather than conditions should be displayed.</param>
        private void EditRule(int selectedIndex, bool showActionPage)
        {
            Debug.Assert(rulesListView.SelectedIndex >= 0 && rulesListView.SelectedIndex < Rules.Count, "index");

            NotificationRuleItem updatedRule = ShowAddEditDialog(Rules[selectedIndex], showActionPage);

            if (updatedRule == null)
            {
                return; // Dialog was cancelled.
            }

            // Replace the selected rule with the updated one and update the description.  Note that
            // updating an entry sets rulesListView.SelectedIndex to -1 so need to re-assign this.
            Rules[selectedIndex] = updatedRule;
            rulesListView.SelectedIndex = selectedIndex;

            GenerateRuleDescription();
        }

        /// <summary>
        /// True if the edit and delete buttons should be enabled.
        /// </summary>
        private void EditAndDeleteCommandsCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = rulesListView != null && rulesListView.SelectedIndex >= 0;
        }

        /// <summary>
        /// Helper method to display the add/edit dialog.
        /// </summary>
        /// <param name="rule">The rule to edit or null to add a new rule.</param>
        /// <param name="showActionPage">True to switch to the action page; false otherwise.</param>
        /// <returns>
        /// The edited/added rule or null if the dialog was cancelled.  Note that
        /// the returned rule is not the same instance as what was passed in.
        /// </returns>
        private NotificationRuleItem ShowAddEditDialog(NotificationRuleItem rule, bool showActionPage)
        {
            AddEditNotificationRuleDialog addEdit =
                new AddEditNotificationRuleDialog(rule, showActionPage);

            addEdit.Owner = WPFHelpers.GetAncestorItemFromObject<Window>(Parent);

            bool? result = addEdit.ShowDialog();

            Debug.Assert(result != null, "result");

            return result.Value ? addEdit.UpdatedRule : null;
        }

        /// <summary>
        /// Command handler for deleting a rule.
        /// </summary>
        private void DeleteRule(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
            Debug.Assert(rulesListView != null, "rulesListView");
            Debug.Assert(rulesListView.SelectedIndex >= 0 && rulesListView.SelectedIndex < Rules.Count, "index");

            if (!Rules.DoNotPromptUserOnDelete)
            {
                MessageBoxResult result =
                    MessageBox.Show(Properties.Resources.RulesDialogDeleteWarning,
                                    Properties.Resources.RulesDialogTitle,
                                    MessageBoxButton.YesNo,
                                    MessageBoxImage.Warning,
                                    MessageBoxResult.Yes,
                                    MessageBoxOptions.None);

                if (result == MessageBoxResult.No)
                {
                    return;
                }
            }

            int currentIndex = rulesListView.SelectedIndex;

            NotificationRules rules = Rules; // Cache the reference to the rules collection.

            rules.RemoveAt(currentIndex);

            if (rules.Count > 0)
            {
                rulesListView.SelectedIndex = currentIndex - 1;
            }
            else
            {
                HandleEmptyList();
            }
        }

        /// <summary>
        /// Displays the "List is empty" message and sets state when the rules list is empty.
        /// A dummy rule is created to display this message so the list actually contains this
        /// dummy entry.
        /// </summary>
        private void HandleEmptyList()
        {
            Debug.Assert(rulesListView.Items.Count == 0, "List should be empty");

            _rulesListIsEmpty = true;

            // Create a dummy rule to display the "List is empty" message.
            NotificationRuleItem dummyRule =
                new NotificationRuleItem(MailboxType.Voicemail,
                                         Properties.Resources.RulesDialogEmptyList, 
                                         DateTime.Now, new ObservableCollection<NotificationChannel>());
            
            Rules.Add(dummyRule);

            rulesListView.SelectedIndex = -1; // Prevent the dummy rule from being selected.

            GenerateRuleDescription();
        }

        /// <summary>
        /// Command handler to move a rule up in the list.
        /// </summary>
        private void MoveRuleUp(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
            Debug.Assert(rulesListView.SelectedIndex > 0, "Selected index should be > 0");

            int newIndex = rulesListView.SelectedIndex - 1;

            NotificationRules rules = Rules; // Cache a reference to thr rules collection.

            rules.Move(rulesListView.SelectedIndex, newIndex);

            rulesListView.SelectedIndex = newIndex; // Selected index "follows" the rule.
        }

        /// <summary>
        /// True if the up command can execute.
        /// </summary>
        private void UpCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = rulesListView != null && rulesListView.SelectedIndex > 0;
        }

        /// <summary>
        /// Move the selected rule down one position in the list.
        /// </summary>
        private void MoveRuleDown(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
            Debug.Assert(rulesListView.SelectedIndex < rulesListView.Items.Count - 1, "Selected index should be < last index");

            int newIndex = rulesListView.SelectedIndex + 1;

            NotificationRules rules = Rules; // Cache a reference to thr rules collection.

            rules.Move(rulesListView.SelectedIndex, newIndex);

            rulesListView.SelectedIndex = newIndex; // Selected index "follows" the rule.
        }

        /// <summary>
        /// True if the down button should be enabled.
        /// </summary>
        private void DownCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = rulesListView != null &&
                rulesListView.SelectedIndex >= 0 && 
                rulesListView.SelectedIndex < rulesListView.Items.Count - 1;
        }
        
        /// <summary>
        /// Command handler to enable a rule (invoked by clicking a rule checkbox).
        /// </summary>
        private void EnableRule(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
            Debug.Assert(e.OriginalSource != null, "e.OriginalSource");

            CheckBox enableRule = Helpers.AssertCast<CheckBox>(e.OriginalSource);

            // When a rule is checked/unchecked, also need to see if all the checkboxes 
            // are now checked/unchecked in which case we need to update the "check all" 
            // checkbox.
            CheckBox enableAllRules = GetEnableAllRulesCheckbox();

            if (enableAllRules.IsChecked.Value && !enableRule.IsChecked.Value)
            {
                enableAllRules.IsChecked = false;
            }
            else if (!enableAllRules.IsChecked.Value && AllRulesEnabled)
            {
                enableAllRules.IsChecked = true;               
            }

            // Checking a checkbox requires us to select that row.
            ListViewItem listItem = WPFHelpers.GetAncestorItemFromObject<ListViewItem>(e.OriginalSource);
            int index = rulesListView.ItemContainerGenerator.IndexFromContainer(listItem);
            rulesListView.SelectedIndex = index;
        }

        /// <summary>
        /// Helper method to obtain the "enable all" checkbox from the "enable rule" column.
        /// </summary>
        /// <returns>
        /// The "enable all" checkbox.
        /// </returns>
        private CheckBox GetEnableAllRulesCheckbox()
        {
            DataTemplate data = Resources["EnableAllRules"] as DataTemplate;

            Debug.Assert(data != null, "Could not find EnableAllRules DataTemplate - has this been renamed?");

            StackPanel panel = data.LoadContent() as StackPanel;

            Debug.Assert(panel != null, "StackPanel could not be found for checkbox");
            Debug.Assert(panel.Children != null, "panel.Children");
            Debug.Assert(panel.Children.Count > 0, "panel.Children.Count");

            CheckBox enableAllCheckbox = panel.Children[0] as CheckBox;

            Debug.Assert(enableAllCheckbox != null, "enableAllCheckbox");

            return enableAllCheckbox;
        }

        /// <summary>
        /// True if all rules are enabled.
        /// </summary>
        private bool AllRulesEnabled
        {
            get
            {
                foreach (NotificationRuleItem rule in Rules)
                {
                    if (!rule.IsEnabled)
                    {
                        return false;
                    }
                }           
                return true;
            }
        }

        /// <summary>
        /// True if "enable rule" checkboxes can be enabled.
        /// </summary>
        private void EnableRuleCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = rulesListView != null && rulesListView.SelectedIndex >= 0;
        }

        /// <summary>
        /// Handler for "enable all rules" checkbox being checked.  This checks or
        /// unchecks each rule checkbox with the value of the "enable all" checkbox.
        /// </summary>
        private void EnableAllRules(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            CheckBox enableAll = Helpers.AssertCast<CheckBox>(e.OriginalSource);

            foreach (NotificationRuleItem rule in Rules)
            {
                rule.IsEnabled = enableAll.IsChecked.Value;
            }
        }

        /// <summary>
        /// True if "enable all rules" checkbox can be enabled.
        /// </summary>
        private void EnableAllCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = rulesListView != null && rulesListView.SelectedIndex >= 0;
        }

        /// <summary>
        /// Command handler for the "Ok" button.  Applies the rules and closes the window.
        /// </summary>
        private void ApplyAndClose(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            Apply(sender, e);
            Close();
        }

        /// <summary>
        /// True if Ok button should be enabled.
        /// </summary>
        private void OkCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            // Set e.Execute to true if both apply and cancel are enabled, false otherwise.
            ApplyCommandCanExecute(sender, e);
            if (!e.CanExecute)
            {
                return;
            }
            CancelCommandCanExecute(sender, e);
        }

        /// <summary>
        /// Command handler for the "Cancel" button.
        /// </summary>
        private void Cancel(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            Close();
        }

        /// <summary>
        /// Closes the parent dialog window which hosts this rules viewer control.
        /// </summary>
        /// <remarks>
        /// This method might fail if the user control is embedded in some other container.  For now
        /// this will fire an assertion if that happens.
        /// </remarks>
        private void Close()
        {
            Window window = WPFHelpers.GetAncestorItemFromObject<Window>(Parent);

            window.Close();
        }

        /// <summary>
        /// True if the "Cancel" button should be enabled.
        /// </summary>
        private void CancelCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = true;
        }

        /// <summary>
        /// Command handler the "Apply" button.
        /// </summary>
        private void Apply(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            applyButton.IsEnabled = false;

            try
            {
                // If the rules list is empty, save an empty list to avoid saving out the dummy rule.
                _rulesLoader.Save(_rulesListIsEmpty ? new NotificationRules() : Rules);
            }
            catch (InvalidOperationException 
#if DEBUG
                exn
#endif
                ) // Thrown if an error occurs saving the rules to storage.
            {
                string message = Properties.Resources.RulesDialogSaveWarningMessage;
#if DEBUG
                message += " Exception details: " + exn.ToString();
#endif
                // Inform the user if rules didn't save, but keep the rules dialog open.
                MessageBox.Show(message, 
                                Properties.Resources.RulesDialogTitle, 
                                MessageBoxButton.OK, 
                                MessageBoxImage.Warning);
            }

            applyButton.IsEnabled = true;
        }

        /// <summary>
        /// True if "Apply" button should be enabled.
        /// </summary>
        private void ApplyCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            e.CanExecute = true;
        }

        /// <summary>
        /// Handler for when the ListView is double-clicked.  From this we need to verify that the name
        /// column (and not the checkbox) was double clicked, and if so edit the rule.
        /// </summary>
        private void rulesListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            if (_rulesListIsEmpty)
            {
                return;
            }

            // Find the ListViewItem containing the item that was clicked on, ignoring if the double-click
            // originated from the checkbox.
            ListViewItem listViewItem =
                WPFHelpers.GetAncestorItemFromObject<ListViewItem>(
                    e.OriginalSource, typeof(CheckBox), /*suppressAssertion*/true
                    );

            if (listViewItem == null)
            {
                return; // A checkbox or an area other than a list item (e.g. a header) was clicked.
            }

            NotificationRuleItem rule = (NotificationRuleItem)rulesListView.ItemContainerGenerator.ItemFromContainer(listViewItem);

            EditRule(Rules.IndexOf(rule));
        }

        /// <summary>
        /// Generate the description for the currently selected rule.
        /// </summary>
        private void GenerateRuleDescription()
        {
            FlowDocument doc = new FlowDocument();

            if (_rulesListIsEmpty)
            {
                descriptionTextBox.Document = doc;
                return;
            }

            NotificationRuleItem rule = Rules[rulesListView.SelectedIndex];

            // Generate the condition description and truncate if it exceeds 120 characters.
            string conditionText = 
                NotificationConditionHelper.GenerateConditionDescription(rule, /*maxLength*/120);
            Underline condition = new Underline(new Run(conditionText));
            condition.Cursor = Cursors.Hand;
            condition.Name = "Condition";
            condition.Foreground = Brushes.Blue;
            condition.MouseDown += descriptionTextBox_MouseDown;

            // Generate the action description and truncate if it exceeds 120 characters.
            string actionText = 
                NotificationConditionHelper.GenerateActionDescription(rule.MailboxType, rule.NotificationChannels, rule.JoinNowChannels, /*maxLength*/120);
            Underline action = new Underline(new Run(actionText));
            action.Name = "Action";
            action.Cursor = Cursors.Hand;
            action.Foreground = Brushes.Blue;
            action.MouseDown += descriptionTextBox_MouseDown;

            // Write the description prefix and append the condition and action descriptions.
            string prefix = rule.MailboxType == MailboxType.Voicemail ?
                Properties.Resources.RulesDialogDescriptionVoicemailsPrefix :
                Properties.Resources.RulesDialogDescriptionCalendarPrefix;
            Paragraph paragraph = new Paragraph();
            paragraph.Inlines.Add(new Run(prefix));
            paragraph.Inlines.Add(condition);
            paragraph.Inlines.Add(new Run(", ")); // Separate the two underlined blocks.
            paragraph.Inlines.Add(action);
            doc.Blocks.Add(paragraph);
            descriptionTextBox.Document = doc;         
        }

        /// <summary>
        /// Handles mouse click on the rule description text box to edit the rule if the
        /// condition link is clicked.
        /// </summary>
        void descriptionTextBox_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            if (_rulesListIsEmpty)
            {
                return;
            }

            Underline underline = sender as Underline;

            if (String.IsNullOrEmpty(underline.Name))
            {
                return;
            }

            if (underline.Name == "Condition")
            {
                EditRule(rulesListView.SelectedIndex);
            }
            else if (underline.Name == "Action")
            {
                EditRule(rulesListView.SelectedIndex, /*showActionPage*/true);
            }
            else
            {
                Debug.Fail("Unexpected block of underlined text");
            }
        }
    }
}
